/*
 * Copyright (c) 2010 Pentaho Corporation.  All rights reserved. 
 * This software was developed by Pentaho Corporation and is provided under the terms 
 * of the GNU Lesser General Public License, Version 2.1. You may not use 
 * this file except in compliance with the license. If you need a copy of the license, 
 * please go to http://www.gnu.org/licenses/lgpl-2.1.txt. The Original Code is Pentaho 
 * Data Integration.  The Initial Developer is Pentaho Corporation.
 *
 * Software distributed under the GNU Lesser Public License is distributed on an "AS IS" 
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or  implied. Please refer to 
 * the license for the specific language governing your rights and limitations.
 */
package org.pentaho.di.core.util;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.pentaho.di.core.Const;
import org.pentaho.di.core.Counter;
import org.pentaho.di.core.database.DatabaseMeta;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.xml.XMLHandler;
import org.pentaho.di.repository.Repository;
import org.pentaho.di.repository.ObjectId;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class SerializationHelper {

  private static final String INDENT_STRING = "    ";

  /**
   * This method will perform the work that used to be done by hand in each kettle input meta for: readData(Node stepnode). We handle all primitive types,
   * complex user types, arrays, lists and any number of nested object levels, via recursion of this method.
   * 
   * @param object
   *          The object to be persisted
   * @param node
   *          The node to 'attach' our XML to
   */
  public static void read(Object object, Node node) {
    // get this classes fields, public, private, protected, package, everything
    Field fields[] = object.getClass().getFields();
    
    for (Field field : fields) {

      // ignore fields which are final, static or transient
      if (Modifier.isFinal(field.getModifiers()) || Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
        continue;
      }
      
      // if the field is not accessible (private), we'll open it up so we can operate on it
      if (!field.isAccessible()) {
        field.setAccessible(true);
      }

      // check if we're going to try to read an array
      if (field.getType().isArray()) {
        try {
          // get the node (if available) for the field
          Node fieldNode = XMLHandler.getSubNode(node, field.getName());
          if (fieldNode == null) {
            // doesn't exist (this is possible if fields were empty/null when persisted)
            continue;
          }
          // get the Java classname for the array elements
          String fieldClassName = XMLHandler.getTagAttribute(fieldNode, "class");
          Class<?> clazz = null;
          // primitive types require special handling
          if (fieldClassName.equals("boolean")) {
            clazz = boolean.class;
          } else if (fieldClassName.equals("int")) {
            clazz = int.class;
          } else if (fieldClassName.equals("float")) {
            clazz = float.class;
          } else if (fieldClassName.equals("double")) {
            clazz = double.class;
          } else if (fieldClassName.equals("long")) {
            clazz = long.class;
          } else {
            // normal, non primitive array class
            clazz = Class.forName(fieldClassName);
          }
          // get the child nodes for the field
          NodeList childrenNodes = fieldNode.getChildNodes();

          // create a new, appropriately sized array
          int arrayLength = 0;
          for (int i = 0; i < childrenNodes.getLength(); i++) {
            Node child = childrenNodes.item(i);
            // ignore TEXT_NODE, they'll cause us to have a larger count than reality, even if they are empty
            if (child.getNodeType() != Node.TEXT_NODE) {
              arrayLength++;
            }
          }
          // create a new instance of our array
          Object array = Array.newInstance(clazz, arrayLength);
          // set the new array on the field (on object, passed in)
          field.set(object, array);

          int arrayIndex = 0;
          for (int i = 0; i < childrenNodes.getLength(); i++) {
            Node child = childrenNodes.item(i);
            if (child.getNodeType() == Node.TEXT_NODE) {
              continue;
            }

            // roll through all of our array elements setting them as encountered
            if (String.class.isAssignableFrom(clazz) || Number.class.isAssignableFrom(clazz)) {
              Constructor<?> constructor = clazz.getConstructor(String.class);
              Object instance = constructor.newInstance(XMLHandler.getTagAttribute(child, "value"));
              Array.set(array, arrayIndex++, instance);
            } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) {
              Object value = Boolean.valueOf(XMLHandler.getTagAttribute(child, "value"));
              Array.set(array, arrayIndex++, value);
            } else if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) {
              Object value = Integer.valueOf(XMLHandler.getTagAttribute(child, "value"));
              Array.set(array, arrayIndex++, value);
            } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) {
              Object value = Float.valueOf(XMLHandler.getTagAttribute(child, "value"));
              Array.set(array, arrayIndex++, value);
            } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) {
              Object value = Double.valueOf(XMLHandler.getTagAttribute(child, "value"));
              Array.set(array, arrayIndex++, value);
            } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) {
              Object value = Long.valueOf(XMLHandler.getTagAttribute(child, "value"));
              Array.set(array, arrayIndex++, value);
            } else {
              // create an instance of 'fieldClassName'
              Object instance = clazz.newInstance();
              // add the instance to the array
              Array.set(array, arrayIndex++, instance);
              // read child, the same way as the parent
              read(instance, child);
            }
          }
        } catch (Throwable t) {
          t.printStackTrace();
          // TODO: log this
        }
      } else if (List.class.isAssignableFrom(field.getType())) {
        // handle lists
        try {
          // get the node (if available) for the field
          Node fieldNode = XMLHandler.getSubNode(node, field.getName());
          if (fieldNode == null) {
            // doesn't exist (this is possible if fields were empty/null when persisted)
            continue;
          }
          // get the Java classname for the array elements
          String fieldClassName = XMLHandler.getTagAttribute(fieldNode, "class");
          Class<?> clazz = Class.forName(fieldClassName);

          // create a new, appropriately sized array
          List<Object> list = new ArrayList<Object>();
          field.set(object, list);

          // iterate over all of the array elements and add them one by one as encountered
          NodeList childrenNodes = fieldNode.getChildNodes();
          for (int i = 0; i < childrenNodes.getLength(); i++) {
            Node child = childrenNodes.item(i);
            if (child.getNodeType() == Node.TEXT_NODE) {
              continue;
            }

            // create an instance of 'fieldClassName'
            if (String.class.isAssignableFrom(clazz) || Number.class.isAssignableFrom(clazz) || Boolean.class.isAssignableFrom(clazz)) {
              Constructor<?> constructor = clazz.getConstructor(String.class);
              Object instance = constructor.newInstance(XMLHandler.getTagAttribute(child, "value"));
              list.add(instance);
            } else {
              // read child, the same way as the parent
              Object instance = clazz.newInstance();
              // add the instance to the array
              list.add(instance);
              read(instance, child);
            }
          }
        } catch (Throwable t) {
          t.printStackTrace();
          // TODO: log this
        }
      } else {
        // we're handling a regular field (not an array or list)
        try {
          Object value = XMLHandler.getTagValue(node, field.getName());
          if (value == null) {
            continue;
          }
          // System.out.println("Setting " + field.getName() + "(" + field.getType().getSimpleName() + ") = " + value + " on: " + object.getClass().getName());
          if (field.getType().isPrimitive() && "".equals(value)) {
            // skip setting of primitives if we see null
          } else if ("".equals(value)) {
            field.set(object, value);
          } else if (field.getType().isPrimitive()) {
            // special primitive handling
            if (double.class.isAssignableFrom(field.getType())) {
              field.set(object, Double.parseDouble(value.toString()));
            } else if (float.class.isAssignableFrom(field.getType())) {
              field.set(object, Float.parseFloat(value.toString()));
            } else if (long.class.isAssignableFrom(field.getType())) {
              field.set(object, Long.parseLong(value.toString()));
            } else if (int.class.isAssignableFrom(field.getType())) {
              field.set(object, Integer.parseInt(value.toString()));
            } else if (byte.class.isAssignableFrom(field.getType())) {
              field.set(object, value.toString().getBytes());
            } else if (boolean.class.isAssignableFrom(field.getType())) {
              field.set(object, "true".equalsIgnoreCase(value.toString()));
            }
          } else if (String.class.isAssignableFrom(field.getType()) || Number.class.isAssignableFrom(field.getType())) {
            Constructor<?> constructor = field.getType().getConstructor(String.class);
            Object instance = constructor.newInstance(value);
            field.set(object, instance);
          } else {
            // we don't know what we're handling, but we'll give it a shot
            Node fieldNode = XMLHandler.getSubNode(node, field.getName());
            if (fieldNode == null) {
              // doesn't exist (this is possible if fields were empty/null when persisted)
              continue;
            }
            // get the Java classname for the array elements
            String fieldClassName = XMLHandler.getTagAttribute(fieldNode, "class");
            Class<?> clazz = Class.forName(fieldClassName);
            Object instance = clazz.newInstance();
            field.set(object, instance);
            read(instance, fieldNode);
          }
        } catch (Throwable t) {
          // TODO: log this
          t.printStackTrace();
        }
      }
    }
  }

  /**
   * This method will perform the work that used to be done by hand in each kettle input meta for: getXML(). We handle all primitive types, complex user types,
   * arrays, lists and any number of nested object levels, via recursion of this method.
   * 
   * @param object
   * @param buffer
   */
  @SuppressWarnings("unchecked")
public static void write(Object object, int indentLevel, StringBuffer buffer) {

    // don't even attempt to persist
    if (object == null) {
      return;
    }

    // get this classes fields, public, private, protected, package, everything
    Field fields[] = object.getClass().getFields();
    for (Field field : fields) {

      // ignore fields which are final, static or transient
      if (Modifier.isFinal(field.getModifiers()) || Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
        continue;
      }

      // if the field is not accessible (private), we'll open it up so we can operate on it
      if (!field.isAccessible()) {
        field.setAccessible(true);
      }

      try {
        Object fieldValue = field.get(object);
        // no value? null? skip it!
        if (fieldValue == null || "".equals(fieldValue)) {

          continue;
        }
        if (field.getType().isPrimitive() || String.class.isAssignableFrom(field.getType()) || Number.class.isAssignableFrom(field.getType())) {
          indent(buffer, indentLevel);
          buffer.append(XMLHandler.addTagValue(field.getName(), fieldValue.toString()));
        } else if (field.getType().isArray()) {
          // write array values
          int length = Array.getLength(fieldValue);

          // open node (add class name attribute)
          indent(buffer, indentLevel);
          buffer.append("<" + field.getName() + " class=\"" + fieldValue.getClass().getComponentType().getName() + "\">").append(Const.CR);

          for (int i = 0; i < length; i++) {
            Object childObject = Array.get(fieldValue, i);
            // handle all strings/numbers
            if (String.class.isAssignableFrom(childObject.getClass()) || Number.class.isAssignableFrom(childObject.getClass())) {
              indent(buffer, indentLevel + 1);
              buffer.append("<").append(fieldValue.getClass().getComponentType().getSimpleName());
              buffer.append(" value=\"" + childObject.toString() + "\"/>").append(Const.CR);
            } else if (Boolean.class.isAssignableFrom(childObject.getClass()) || boolean.class.isAssignableFrom(childObject.getClass())) {
              // handle booleans (special case)
              indent(buffer, indentLevel + 1);
              buffer.append("<").append(fieldValue.getClass().getComponentType().getSimpleName());
              buffer.append(" value=\"" + childObject.toString() + "\"/>").append(Const.CR);
            } else {
              // array element is a user defined/complex type, recurse into it
              indent(buffer, indentLevel + 1);
              buffer.append("<" + fieldValue.getClass().getComponentType().getSimpleName() + ">").append(Const.CR);
              write(childObject, indentLevel + 1, buffer);
              indent(buffer, indentLevel + 1);
              buffer.append("</" + fieldValue.getClass().getComponentType().getSimpleName() + ">").append(Const.CR);
            }
          }
          // close node
          buffer.append("    </" + field.getName() + ">").append(Const.CR);
        } else if (List.class.isAssignableFrom(field.getType())) {
          // write list values
          List<Object> list = (List<Object>) fieldValue;
          if (list.size() == 0) {
            continue;
          }
          Class<?> listClass = list.get(0).getClass();

          // open node (add class name attribute)
          indent(buffer, indentLevel);
          buffer.append("<" + field.getName() + " class=\"" + listClass.getName() + "\">").append(Const.CR);

          for (Object childObject : list) {
            // handle all strings/numbers
            if (String.class.isAssignableFrom(childObject.getClass()) || Number.class.isAssignableFrom(childObject.getClass())) {
              indent(buffer, indentLevel + 1);
              buffer.append("<").append(listClass.getSimpleName());
              buffer.append(" value=\"" + childObject.toString() + "\"/>").append(Const.CR);
            } else if (Boolean.class.isAssignableFrom(childObject.getClass()) || boolean.class.isAssignableFrom(childObject.getClass())) {
              // handle booleans (special case)
              indent(buffer, indentLevel + 1);
              buffer.append("<").append(listClass.getSimpleName());
              buffer.append(" value=\"" + childObject.toString() + "\"/>").append(Const.CR);
            } else {
              // array element is a user defined/complex type, recurse into it
              indent(buffer, indentLevel + 1);
              buffer.append("<" + listClass.getSimpleName() + ">").append(Const.CR);
              write(childObject, indentLevel + 1, buffer);
              indent(buffer, indentLevel + 1);
              buffer.append("</" + listClass.getSimpleName() + ">").append(Const.CR);
            }
          }
          // close node
          indent(buffer, indentLevel);
          buffer.append("</" + field.getName() + ">").append(Const.CR);
        } else {
          // if we don't now what it is, let's treat it like a first class citizen and try to write it out
          // open node (add class name attribute)
          indent(buffer, indentLevel);
          buffer.append("<" + field.getName() + " class=\"" + fieldValue.getClass().getName() + "\">").append(Const.CR);
          write(fieldValue, indentLevel + 1, buffer);
          // close node
          indent(buffer, indentLevel);
          buffer.append("</" + field.getName() + ">").append(Const.CR);
        }
      } catch (Throwable t) {
        t.printStackTrace();
        // TODO: log this
      }
    }

  }

  /**
   * Handle saving of the input (object) to the kettle repository using the most simple method available, by calling write and then saving the job-xml as a
   * job attribute.
   * 
   * @param object
   * @param rep
   * @param id_transformation
   * @param id_step
   * @throws KettleException
   */
  public static void saveJobRep(Object object, Repository rep, ObjectId id_job, ObjectId id_job_entry) throws KettleException {
    StringBuffer sb = new StringBuffer(1024);
    write(object, 0, sb);
    rep.saveJobEntryAttribute(id_job, id_job_entry, "job-xml", sb.toString());
  }

  /**
   * Handle reading of the input (object) from the kettle repository by getting the job-xml from the repository step attribute string and then re-hydrate the
   * job entry (object) with our already existing read method.
   * 
   * @param object
   * @param rep
   * @param id_step
   * @param databases
   * @throws KettleException
   */
  public static void readJobRep(Object object, Repository rep, ObjectId id_step, List<DatabaseMeta> databases) throws KettleException {
    try {
      String jobXML = rep.getJobEntryAttributeString(id_step, "job-xml");
      ByteArrayInputStream bais = new ByteArrayInputStream(jobXML.getBytes());
      Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(bais);
      read(object, doc.getDocumentElement());
    } catch (ParserConfigurationException ex) {
      throw new KettleException(ex.getMessage(), ex);
    } catch (SAXException ex) {
      throw new KettleException(ex.getMessage(), ex);
    } catch (IOException ex) {
      throw new KettleException(ex.getMessage(), ex);
    }
  }
  
  
  /**
   * Handle saving of the input (object) to the kettle repository using the most simple method available, by calling write and then saving the step-xml as a
   * step attribute.
   * 
   * @param object
   * @param rep
   * @param id_transformation
   * @param id_step
   * @throws KettleException
   */
  public static void saveStepRep(Object object, Repository rep, ObjectId id_transformation, ObjectId id_step) throws KettleException {
    StringBuffer sb = new StringBuffer(1024);
    write(object, 0, sb);
    rep.saveStepAttribute(id_transformation, id_step, "step-xml", sb.toString());
  }

  /**
   * Handle reading of the input (object) from the kettle repository by getting the step-xml from the repository step attribute string and then re-hydrate the
   * step (object) with our already existing read method.
   * 
   * @param object
   * @param rep
   * @param id_step
   * @param databases
   * @param counters
   * @throws KettleException
   */
  public static void readStepRep(Object object, Repository rep, ObjectId id_step, List<DatabaseMeta> databases, Map<String, Counter> counters) throws KettleException {
    try {
      String stepXML = rep.getStepAttributeString(id_step, "step-xml");
      ByteArrayInputStream bais = new ByteArrayInputStream(stepXML.getBytes());
      Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(bais);
      read(object, doc.getDocumentElement());
    } catch (ParserConfigurationException ex) {
      throw new KettleException(ex.getMessage(), ex);
    } catch (SAXException ex) {
      throw new KettleException(ex.getMessage(), ex);
    } catch (IOException ex) {
      throw new KettleException(ex.getMessage(), ex);
    }
  }

  private static void indent(StringBuffer sb, int indentLevel) {
    for (int i = 0; i < indentLevel; i++) {
      sb.append(INDENT_STRING);
    }
  }

}
